/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using Anculus.Core;

using Galaxium.Core;

namespace Galaxium.Protocol.Msn
{
	public abstract class DirectConnection
	{
		const int _receiveBufferSize = 1024;
		
		public event EventHandler Connected;
		public event EventHandler Disconnected;
		public event EventHandler<ByteArrayEventArgs> DataReceived;
		public event EventHandler<ObjectEventArgs> SendComplete;
		
		SocketType _sockType;
		ProtocolType _protType;
		IPAddress _address;
		int _port;
		
		bool _listen;
		Socket _listenSocket;
		Socket _socket;
		byte[] _receiveBuffer = new byte [_receiveBufferSize];
		int _sending = 0;
		bool _intendedDisconnect = false;
		
		EndPoint _remoteEndPoint;
		
		public bool IsConnected
		{
			get { return _socket != null; }
		}
		
		public bool IsSending
		{
			get { return _sending > 0; }
		}
		
		public EndPoint RemoteEndPoint
		{
			get { return _remoteEndPoint; }
		}
		
		public bool LocalServer
		{
			get { return _listen; }
		}
		
		protected DirectConnection (bool listen, IPAddress address, int port, SocketType sockType, ProtocolType protType)
		{
			_listen = listen;
			_address = address;
			_port = port;
			_sockType = sockType;
			_protType = protType;
			
			if (listen)
				Listen ();
			else
				Connect ();
		}
		
		public void Connect ()
		{
			Log.Debug ("Connect to {0} port {1}", _address, _port);
			
			_remoteEndPoint = new IPEndPoint (_address, _port);
			
			try
			{
				_socket = new Socket (AddressFamily.InterNetwork, _sockType, _protType);
				_socket.BeginConnect (_remoteEndPoint, new AsyncCallback (ConnectCallback), this);
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Error connecting socket");
			}
		}
		
		public void Disconnect ()
		{
			_intendedDisconnect = true;
			
			StopListening ();
			
			if (_socket != null)
			{
				try
				{
					if (_socket.Connected)
					{
						_socket.Shutdown (SocketShutdown.Both);
						_socket.Close ();
					}
				}
				catch (Exception ex)
				{
					Log.Error (ex, "Unable to cleanly disconnect socket");
				}
				
				_socket = null;
			}
			
			OnDisconnected ();
			
			_intendedDisconnect = false;
		}
		
		void ConnectCallback (IAsyncResult asyncResult)
		{
			try
			{
				_socket.EndConnect (asyncResult);
				
				Log.Debug ("Successfully connected");
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnConnected ();
				}));
				
				_socket.BeginReceive (_receiveBuffer, 0, _receiveBufferSize, 0, new AsyncCallback (ReceiveCallback), this);
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Error during connection");
			}
		}
				
#region Listening
		void Listen ()
		{
			Log.Debug ("Listen on {0} port {1}", _address, _port);
			
			_listenSocket = new Socket (AddressFamily.InterNetwork, _sockType, _protType);
			
			try
			{
				_listenSocket.Bind (new IPEndPoint (_address, _port));
				_listenSocket.Listen (1);
				
				_listenSocket.BeginAccept (new AsyncCallback (AcceptCallback), this);
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Error listening for connection");
			}
		}
		
		public void StopListening ()
		{
			if (_listenSocket != null)
			{
				try
				{
					_listenSocket.Close ();
					_listenSocket = null;
				}
				catch (SocketException ex)
				{
					Log.Error (ex, "Unable to cleanly close listen socket");
				}
			}
		}
		
		void AcceptCallback (IAsyncResult asyncResult)
		{
			try
			{
				_socket = _listenSocket.EndAccept (asyncResult);
				_socket.BeginReceive (_receiveBuffer, 0, _receiveBufferSize, 0, new AsyncCallback (ReceiveCallback), this);
				
				_remoteEndPoint = _socket.RemoteEndPoint;
				
				StopListening ();
				
				Log.Debug ("Connection from {0} established", _remoteEndPoint);
				
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					OnConnected ();
				}));
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Error accepting connection");
			}
		}
		
		void ReceiveCallback (IAsyncResult asyncResult)
		{
			try
			{
				int read = _socket.EndReceive (asyncResult);
				
				if (read > 0)
				{
					byte[] data = new byte[read];
					
					lock (this)
						Array.Copy (_receiveBuffer, data, read);
					
					ThreadUtility.SyncDispatch (new VoidDelegate (delegate
					{
						OnDataReceived (new ByteArrayEventArgs (data));
					}));
				}
				else
					throw new ApplicationException ();
				
				// _socket could be null if we disconnect on receipt of data
				if (_socket != null)
				{
					// Wait for more data
					_socket.BeginReceive (_receiveBuffer, 0, _receiveBufferSize, 0, new AsyncCallback (ReceiveCallback), this);
				}
			}
			catch (Exception ex)
			{
				if (!_intendedDisconnect)
					Log.Error (ex, "Unexpected disconnect");
				
				Disconnect ();
			}
		}
#endregion
		
#region Sending
		public void Send (byte[] data)
		{
			Send (data, null);
		}
		
		public void Send (byte[] data, object userData)
		{
			if (_socket == null)
			{
				Log.Error ("Send called with no connection present");
				return;
			}
			
			SendState state = new SendState (userData);
			
			_sending++;
			_socket.BeginSend (data, 0, data.Length, 0, new AsyncCallback (SendCallback), state);
		}
		
		protected void SendCallback (IAsyncResult asyncResult)
		{
			SendState state = asyncResult.AsyncState as SendState;
			
			try
			{
				_socket.EndSend (asyncResult);
				
				try
				{
					OnSendComplete (new ObjectEventArgs (state.UserData));
				}
				catch
				{
				}
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Error sending data");
				
				Disconnect ();
			}
			finally
			{
				_sending--;
			}
		}
#endregion
		
		protected virtual void OnConnected ()
		{
			if (Connected != null)
				Connected (this, EventArgs.Empty);
		}
		
		protected virtual void OnDisconnected ()
		{
			if (Disconnected != null)
				Disconnected (this, EventArgs.Empty);
		}
		
		protected virtual void OnDataReceived (ByteArrayEventArgs args)
		{
			if (DataReceived != null)
				DataReceived (this, args);
		}
		
		protected virtual void OnSendComplete (ObjectEventArgs args)
		{
			if (SendComplete != null)
				SendComplete (this, args);
		}
		
		class SendState
		{
			public object UserData;
			
			public SendState (object userData)
			{
				UserData = userData;
			}
		}
	}
}
